using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using DslCrossModelFramework;
using DslCrossModelFramework.Environment;
using DslCrossModelFramework.ModelRelationship;
using DslCrossModelFramework.NameResolution;
using DslCrossModelFramework.VSPackage;
using EnvDTE;
using Microsoft.VisualStudio.Modeling;

namespace DslCrossModelFramework {
    /// <summary>
    /// Implementation of <see cref="IModelDirectory"/> used for retrieving model objects
    /// </summary>
    public class ModelDirectory : IModelDirectory {

        private readonly List<DomainModelInfo> metaDataLoaded = new List<DomainModelInfo>();
        private readonly Dictionary<Guid,ModelClass> modelElementMetaData = new Dictionary<Guid,ModelClass>();
        private readonly DomainXmlSerializerDirectory xmlSerializerDirectory = new DomainXmlSerializerDirectory();


        /// <summary>
        /// Gets the model files.
        /// </summary>
        /// <value>The model files.</value>
        public List<IModelFileInfo> ModelFiles {
            get { return EnvironmentConfiguration.Current.ModelFiles; }
        }

        /// <summary>
        /// Gets the name of the current solution.
        /// </summary>
        /// <value>The name of the current solution.</value>
        public string CurrentSolutionName {
            get {
                DTE service = DSLCrossModelFrameworkPackage.GetGlobalService(typeof (DTE)) as DTE;
                if (service != null) {
                    return service.Solution != null ? Path.GetFileNameWithoutExtension(service.Solution.FileName) : "Solution";
                }
                else {
                    return "";
                }
            }
        }

        /// <summary>
        /// Preloads all model files.
        /// </summary>
        public void PreloadAllModelFiles() {
            foreach (ModelFileInfo info in EnvironmentConfiguration.Current.ModelFiles) {
                info.Load();
            }
            
        }

        /// <summary>
        /// Determines whether the specified model file is loaded.
        /// </summary>
        /// <param name="modelFile">The model file.</param>
        /// <returns>
        /// 	<c>true</c> if the specified model file is loaded; otherwise, <c>false</c>.
        /// </returns>
        private bool IsModelFileLoaded(IModelFile modelFile) {

            return false;
        }


        private List<DslToolsPackageInfo> packages;

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelDirectory"/> class.
        /// </summary>
        public ModelDirectory() : this( EnvironmentConfiguration.Current.GetTempModelFileHelper() ) {}

        /// <summary>
        /// Initializes a new instance of the <see cref="ModelDirectory"/> class.
        /// </summary>
        /// <param name="modelFileHelper">The model file helper.</param>
        public ModelDirectory(IModelFileHelper modelFileHelper) {
            EnvironmentConfiguration.Current.ModelFileAdded += Environment_ModelFileAdded;
            EnvironmentConfiguration.Current.ModelFileLoaded += Environment_ModelFileLoaded;
            EnvironmentConfiguration.Current.ModelFileUnloaded += Environment_ModelFileUnloaded;
            EnvironmentConfiguration.Current.ModelFileRemoved += Environment_ModelFileRemoved;
            EnvironmentConfiguration.Current.ModelFileUpdated += Environment_ModelFileUpdated;
            using (modelFileHelper) {
                packages = EnvironmentConfiguration.Current.Packages;
                foreach (DslToolsPackageInfo package in packages) {
                    if (!string.IsNullOrEmpty(package.SerializationBehaviorType)) {
                        xmlSerializerDirectory.AddBehavior(Type.GetType(package.SerializationBehaviorType).InvokeMember("Instance", BindingFlags.GetProperty, null, null, null, CultureInfo.CurrentCulture) as DomainXmlSerializationBehavior);
                    }
                }
            }
        }

        void Environment_ModelFileUpdated(object sender, ModelFileEventArgs e) {
            if(e.FileInfo.IsLoaded && !e.FileInfo.IsDynamic) {
                UnloadModel(e.FileInfo);
                LoadModelFile(e.FileInfo.ModelFile, e.FileInfo.Store);
            }
        }

        void Environment_ModelFileRemoved(object sender, ModelFileEventArgs e) {
            
        }

        void Environment_ModelFileUnloaded(object sender, ModelFileEventArgs e) {
            
        }

        void Environment_ModelFileLoaded(object sender, ModelFileEventArgs e) {
            if(e.FileInfo.IsLoaded) {
                LoadCustomMetaData(e.FileInfo.ModelRoot);
            }
        }

        void Environment_ModelFileAdded(object sender, ModelFileEventArgs e) {
            if (e.FileInfo.IsLoaded) {
                LoadCustomMetaData(e.FileInfo.ModelRoot);
            }
        } 

        internal ModelElement LoadModelFile(IModelFile modelFile, Store store) {
            foreach (DslToolsPackageInfo info in packages) {
                if(modelFile.DslPackageId == info.Guid) {
                    return LoadModelFile(modelFile, info, store);
                }
            }
            return null;
        }

        internal ModelElement LoadModelFile(IModelFile modelFile, DslToolsPackageInfo package, Store store) {
            return LoadModelFile(modelFile, new ModelFileHelper(), package, store);
        }

        internal ModelElement LoadModelFile(IModelFile modelFile, IModelFileHelper modelFileHelper, DslToolsPackageInfo package, Store store) {
            ModelElement root = null;
            if (!IsModelFileLoaded(modelFile)) {
                root = modelFileHelper.LoadModelRoot(store, package, modelFile);
                LoadCustomMetaData(root);
            }
            return root;
        }

        /// <summary>
        /// Unloads the model.
        /// </summary>
        /// <param name="modelFileInfo">The model file info.</param>
        public void UnloadModel(IModelFileInfo modelFileInfo) {
            if(!modelFileInfo.IsDynamic) {
                modelFileInfo.Unload();
            }
        }

        /// <summary>
        /// Loads the custom meta data.
        /// </summary>
        private void LoadCustomMetaData(ModelElement modelRoot) {
            // TODO: Add meta data via Domain Class
            if(!metaDataLoaded.Contains(modelRoot.GetDomainClass().DomainModel)) {
                foreach (DomainClassInfo info in modelRoot.GetDomainClass().DomainModel.DomainClasses) {
                    if (!modelElementMetaData.ContainsKey(info.Id)) {
                        modelElementMetaData.Add(info.Id, new ModelClass(info, modelRoot.GetDomainClass()));
                    }
                    Type domainObjectType = info.ImplementationClass;
                    object[] attributes = domainObjectType.GetCustomAttributes(true);
                    foreach (object attribute in attributes) {
                        if(attribute is ModelElementNameResolutionAttribute) {
                            // Register the defined name resolvers
                            Type nameResolverType = ((ModelElementNameResolutionAttribute) attribute).NameResolver;
                            if(nameResolverType.GetInterface(typeof(IModelElementNameResolver).Name)==null) {
                                throw new NameResolutionException(
                                    string.Format("Name resolver '{0}' must implement IModelElementNameResolver",
                                                  nameResolverType.FullName));

                            }
                            ConstructorInfo constructor = nameResolverType.GetConstructor(new Type[] {});
                            if(constructor == null) {
                                throw new NameResolutionException(
                                    string.Format("Name resolver '{0}' must have a default constructor",
                                                  nameResolverType.FullName));
                            }
                            NameResolutionManager.Current.RegisterNameResolver(constructor.Invoke(null) as IModelElementNameResolver, ((ModelElementNameResolutionAttribute)attribute).Scope);
                        } else if (attribute is DslElementMetaDataAttribute) { 
                            // Register the serialization meta data
                            if (modelElementMetaData.ContainsKey(info.Id)) {
                                modelElementMetaData[info.Id].SerializesId = ((DslElementMetaDataAttribute)attribute).SerializesId;
                                modelElementMetaData[info.Id].HasMonikerKey = ((DslElementMetaDataAttribute)attribute).HasMonikerKey;
                                modelElementMetaData[info.Id].HasElementName = ((DslElementMetaDataAttribute)attribute).HasElementName;
                            }
                        } else if (attribute is RelationshipDefinitionAttribute) { 
                            // Register the serialization meta data
                            // Register the defined relationship definition
                            Type resolverType = ((RelationshipDefinitionAttribute)attribute).RelationshipDefinitionType;
                            if (resolverType.GetInterface(typeof(IModelRelationshipDefinition).Name) == null) {
                                throw new ModelRelationshipException(
                                    string.Format("Model relationship '{0}' must implement IModelRelationshipDefinition",
                                                  resolverType.FullName));

                            }
                            ConstructorInfo constructor = resolverType.GetConstructor(new Type[] { });
                            if (constructor == null) {
                                throw new ModelRelationshipException(
                                    string.Format("Model relationship '{0}' must have a default constructor",
                                                  resolverType.FullName));
                            }
                            ModelRelationshipManager.Current.RegisterRelationship(constructor.Invoke(null) as IModelRelationshipDefinition, ((RelationshipDefinitionAttribute)attribute).Scope);
                        }
                    }
                }
                metaDataLoaded.Add(modelRoot.GetDomainClass().DomainModel);
            }
        }


        /// <summary>
        /// Gets the model root.
        /// </summary>
        /// <param name="modelFile">The model file.</param>
        /// <returns></returns>
        public ModelElement GetModelRoot(IModelFile modelFile) {
            foreach (ModelFileInfo currentModelFileInfo in ModelFiles) {
                if (currentModelFileInfo.ModelFile.Equals(modelFile)) {
                    return currentModelFileInfo.ModelRoot;
                }
            }
            return null;
        }

        /// <summary>
        /// Gets the model file info.
        /// </summary>
        /// <param name="modelElement">The model element.</param>
        /// <returns></returns>
        public IModelFileInfo GetModelFileInfo(ModelElement modelElement) {
            ModelElement root = GetModelElementRoot(modelElement);
            foreach (ModelFileInfo modelFileInfo in EnvironmentConfiguration.Current.ModelFiles) {
                if(modelFileInfo.IsLoaded) {
                    if(GetModelElementReference(modelFileInfo.ModelRoot) == GetModelElementReference(root)) {
                        return modelFileInfo;
                    }
                }
            }
            return null;
        }

        /// <summary>
        /// Determines whether an instance of the specified domain class info can be referenced in a serialized model path.
        /// </summary>
        /// <param name="domainClassInfo">The domain class info.</param>
        /// <returns>
        /// 	<c>true</c> if an instance of the specified domain class info can be referenced in a serialized model path; otherwise, <c>false</c>.
        /// </returns>
        public bool CanReferenceModelElement(DomainClassInfo domainClassInfo) {
            if(domainClassInfo is DomainRelationshipInfo) {
                DomainRelationshipInfo relationshipInfo = domainClassInfo as DomainRelationshipInfo;
                if(xmlSerializerDirectory.GetSerializer(relationshipInfo.Id) != null && ((DomainRelationshipXmlSerializer) xmlSerializerDirectory.GetSerializer(relationshipInfo.Id)).SerializesId) {
                    // The Relationship serializes its id, so ok.
                    return true;
                }
                bool canReferenceRoles = true;
                foreach (DomainRoleInfo role in relationshipInfo.DomainRoles) {
                    if(!CanReferenceModelElement(role.RolePlayer)) {
                        // The role player cannot be referenced so we cannot reference this link using hash
                        canReferenceRoles = false;
                        break;
                    }
                }
                return canReferenceRoles;
            }
            else {
                if(!modelElementMetaData.ContainsKey(domainClassInfo.Id)) {
                    // No meta data loaded for this type
                    return false;
                }

                // Can only reference if following conditions are met
                ModelClass modelClass = modelElementMetaData[domainClassInfo.Id];
                return modelClass.SerializesId || modelClass.HasElementName || modelClass.HasMonikerKey;
            }
        }

        /// <summary>
        /// Gets the type of the model element root.
        /// </summary>
        /// <param name="domainClassInfo">The domain class info.</param>
        /// <returns></returns>
        public DomainClassInfo GetModelElementRootType(DomainClassInfo domainClassInfo) {
            return this.modelElementMetaData[domainClassInfo.Id].RootClassInfo;
        }


        /// <summary>
        /// Gets all model elements.
        /// </summary>
        /// <value>All model elements.</value>
        public ReadOnlyCollection<ModelElement> AllModelElements {
            get {
                List<ModelElement> modelElements = new List<ModelElement>();
                foreach(IModelFileInfo fileInfo in ModelFiles) {
                    if(fileInfo.IsLoaded) {
                        modelElements.AddRange(fileInfo.AllElements());
                    }
                }
                return new ReadOnlyCollection<ModelElement>(new List<ModelElement>(modelElements));
            }
        }

        /// <summary>
        /// Finds all model elements of the specified type.
        /// </summary>
        /// <param name="domainClassId">The domain class id of the type.</param>
        /// <returns>A readonly collection of ModelElements</returns>
        public ReadOnlyCollection<ModelElement> FindAllElements(Guid domainClassId) {
            List<ModelElement> foundMels = new List<ModelElement>();
            List<Guid> coveredStores = new List<Guid>();
            foreach (IModelFileInfo modelFile in EnvironmentConfiguration.Current.ModelFiles) {
                if(modelFile.IsLoaded) {
                    if(!coveredStores.Contains(modelFile.Store.Id)) {
                        foundMels.AddRange(modelFile.Store.ElementDirectory.FindElements(domainClassId));
                    }
                }
            }
            return new ReadOnlyCollection<ModelElement>(foundMels);
        }

        /// <summary>
        /// Finds a model element.
        /// </summary>
        /// <param name="domainInstanceId">The domain instance id of the model element.</param>
        /// <returns>The Model Element</returns>
        public ModelElement FindElement(Guid domainInstanceId) {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Finds a model element.
        /// </summary>
        /// <param name="domainClassId">The domain class id of the model element type.</param>
        /// <param name="domainInstanceId">The domain instance id of the model element.</param>
        /// <returns>The model element if found, otherwise null</returns>
        public ModelElement FindElement(Guid domainClassId, Guid domainInstanceId) {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Gets the element specified by the id.
        /// </summary>
        /// <param name="elementId">The element id.</param>
        /// <returns></returns>
        public ModelElement GetElement(Guid elementId) {
            foreach(IModelFileInfo modelFile in EnvironmentConfiguration.Current.ModelFiles) {
                ModelElement foundElement = modelFile.Store.ElementDirectory.GetElement(elementId);
                if(foundElement != null) {
                    return foundElement;
                }
            }
            return null;
        }

        /// <summary>
        /// Gets all the elements found.
        /// </summary>
        /// <returns></returns>
        public ReadOnlyCollection<ModelElement> AllElements() {
            return AllModelElements;
        }

        /// <summary>
        /// Finds all elements in a model.
        /// </summary>
        /// <param name="modelRoot">The model root.</param>
        /// <returns></returns>
        public ReadOnlyCollection<ModelElement> FindAllElementsInModel(ModelElement modelRoot) {
            foreach(IModelFileInfo modelFileInfo in ModelFiles) {
                if(modelFileInfo.IsLoaded) {
                    if(modelRoot.Id == modelFileInfo.ModelRoot.Id) {
                        return modelFileInfo.AllElements();
                    }
                }
            }
            return new ReadOnlyCollection<ModelElement>(new List<ModelElement>());
        }

        /// <summary>
        /// Gets all model roots.
        /// </summary>
        /// <returns></returns>
        public ReadOnlyCollection<ModelElement> GetAllModelRoots() {
            List<ModelElement> modelRoots = new List<ModelElement>();
            foreach(IModelFileInfo modelFileInfo in ModelFiles) {
                modelRoots.Add(modelFileInfo.ModelRoot);
            }

            return new ReadOnlyCollection<ModelElement>(modelRoots);
        }

        public ModelElement GetModelElementRoot(ModelElement modelElement) {
            foreach(IModelFileInfo modelFileInfo in ModelFiles) {
                if( modelFileInfo.Contains(modelElement)) {
                    return modelFileInfo.ModelRoot;
                }
            }
            throw new ModelElementLocateRootException(modelElement);
        }

        public DslToolsPackageInfo GetPackageInfo(ModelElement modelElement) {
            foreach(DslToolsPackageInfo packageInfo in EnvironmentConfiguration.Current.Packages) {
                if(packageInfo.DomainModelType == modelElement.GetType().AssemblyQualifiedName) {
                    return packageInfo;
                }
            }
            ModelElement modelRoot = GetModelElementRoot(modelElement);
            if(modelRoot != null) {
                foreach (DslToolsPackageInfo packageInfo in packages) {
                    if (packageInfo.DomainModelType == modelRoot.GetType().AssemblyQualifiedName) {
                        return packageInfo;
                    }
                }
            }
            throw new ArgumentException(string.Format("Unknown model element type '{0}'",modelElement.GetType()));
        }

        /// <summary>
        /// Resolves the model element reference.
        /// </summary>
        /// <param name="modelReference">The model reference.</param>
        /// <param name="modelFile">The model file.</param>
        /// <returns></returns>
        public ModelElement ResolveModelElementReference(string modelReference, IModelFile modelFile) {
            // Get the domain class type id
            if(!modelReference.StartsWith("(")) {
                throw new ArgumentException("Model Reference must begin with '('");
            }
            foreach(IModelFileInfo modelFileInfo in ModelFiles) {
                if (modelFile != null && !modelFile.Equals(modelFileInfo.ModelFile)) {
                    continue;
                }
                string elementClassIdString = modelReference.Substring(1, modelReference.IndexOf(")") - 1);
                string modelElementReference = modelReference.Substring(modelReference.IndexOf(")") + 1);
                Regex isGuidRegex = new Regex(@"^\{?[a-fA-F\d]{8}-([a-fA-F\d]{4}-){3}[a-fA-F\d]{12}\}?$", RegexOptions.Singleline | RegexOptions.IgnoreCase);
                if (isGuidRegex.Matches(modelElementReference).Count == 1) {
                    ModelElement mel = modelFileInfo.Store.ElementDirectory.GetElement(new Guid(isGuidRegex.Matches(modelElementReference)[0].Value));
                    if (mel != null) {
                        return mel;
                    }
                } else {
                    // Check if the modelElementReference is a guid
                    Guid elementClassId = new Guid(elementClassIdString);
                    foreach (ModelElement mel in modelFileInfo.Store.ElementDirectory.FindElements(elementClassId)) {
                        if (GetModelElementReference(mel) == modelReference) {
                            return mel;
                        }
                    }
                
                }
            }
            throw new ModelReferenceResolutionException(modelReference);
        }

        /// <summary>
        /// Gets the model element reference.
        /// </summary>
        /// <param name="modelElement">The model element.</param>
        /// <returns></returns>
        public string GetModelElementReference(ModelElement modelElement) {
            string strRef = modelElement.Id.ToString();
            if (!modelElementMetaData.ContainsKey(modelElement.GetDomainClass().Id) || !modelElementMetaData[modelElement.GetDomainClass().Id].SerializesId) {


                DomainClassXmlSerializer ser = xmlSerializerDirectory.GetSerializer(modelElement.GetDomainClass().Id);
                strRef = ser.CalculateQualifiedName(xmlSerializerDirectory, modelElement);

                if (modelElement is ElementLink) {
                    // We have a link so we need to determine if this is a unique reference
                    if (!((DomainRelationshipXmlSerializer)ser).SerializesId) {
                        // We have a non-unique ref built from a moniker not using just the serialized id
                        // We must now build up a hash of the merged role players references to get a unique reference
                        List<string> rolePlayingMelRefs = new List<string>();
                        foreach (DomainRoleInfo roleInfo in ((ElementLink)modelElement).GetDomainRelationship().DomainRoles) {
                            ModelElement rolePlayingMel = roleInfo.GetRolePlayer((ElementLink)modelElement);
                            rolePlayingMelRefs.Add(GetModelElementReference(rolePlayingMel));
                        }
                        string joinedRoles = string.Join("*", rolePlayingMelRefs.ToArray());
                        byte[] hash = MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(joinedRoles));
                        strRef = Convert.ToBase64String(hash);
                        // Replace the chars we use for positional finding
                        strRef = strRef.Replace('[', '*').Replace(']', '*').Replace('|', '*');
                    }
                }
            }
            // Finally, add the class info guid to speed things up when we are searching for a match
            return string.Format("({0}){1}",modelElement.GetDomainClass().Id, strRef);
        }

        /// <summary>
        /// Gets all model root types.
        /// </summary>
        /// <returns></returns>
        public List<Type> GetAllModelRootTypes() {
            List<Type> types = new List<Type>();
            foreach (DslToolsPackageInfo packageInfo in packages) {
                types.Add(Type.GetType(packageInfo.ModelElementRootType));
            }
            return types;
        }

        public List<Type> GetAllModelTypes() {
            List<Type> types = new List<Type>();
            foreach (DomainClassInfo info in GetAllClassesInDomains()) {
                types.Add(info.ImplementationClass);
            }
            return types;
        }

        /// <summary>
        /// Gets all DomainClassInfo objects for the types of classes in a domain type.
        /// </summary>
        /// <param name="domainModelRootType">A domain model root.</param>
        /// <returns></returns>
        internal ReadOnlyCollection<DomainClassInfo> GetAllClassesInDomain(Type domainModelRootType) {
            foreach (DslToolsPackageInfo packageInfo in packages) {
                if (packageInfo.ModelElementRootType == domainModelRootType.AssemblyQualifiedName) {
                    foreach (IModelFileInfo modelFileInfo in ModelFiles) {
                        foreach (DomainModel domainModel in modelFileInfo.Store.DomainModels) {
                            if (domainModel.DomainModelInfo.DomainModel.ImplementationType.AssemblyQualifiedName ==
                                packageInfo.DomainModelType) {
                                return domainModel.DomainModelInfo.DomainClasses;
                            }
                        }
                    }
                }
            }

            return new ReadOnlyCollection<DomainClassInfo>(new List<DomainClassInfo>());
        }

        internal ReadOnlyCollection<DomainClassInfo> GetAllClassesInDomains() {
            foreach (DslToolsPackageInfo packageInfo in packages) {
                foreach (IModelFileInfo modelFileInfo in ModelFiles) {
                    foreach (DomainModel domainModel in modelFileInfo.Store.DomainModels) {
                        if (domainModel.DomainModelInfo.DomainModel.ImplementationType.AssemblyQualifiedName ==
                            packageInfo.DomainModelType) {
                            return domainModel.DomainModelInfo.DomainClasses;
                        }
                    }
                }
            }

            return new ReadOnlyCollection<DomainClassInfo>(new List<DomainClassInfo>());
        }
    }

    internal class ModelReferenceResolutionException : Exception {
        public ModelReferenceResolutionException(string reference) : base(string.Format("Could not resolve the reference {0}",reference)) {}
    }

    /// <summary>
    /// Occurs when the model directory is asked to locate the model root for a given element and it cannot be found
    /// </summary>
    public class ModelElementLocateRootException : Exception {
        /// <summary>
        /// Initializes a new instance of the <see cref="ModelElementLocateRootException"/> class.
        /// </summary>
        /// <param name="childModelElement">The child model element.</param>
        public ModelElementLocateRootException(ModelElement childModelElement) : base(string.Format("Could not find the root element for the model element '{0}'",NameResolutionManager.Current.GetElementName(childModelElement))) {}
    }
}